Skip to content

Commit f1ce95e

Browse files
committed
Shared/Date::isDateTime Handle Cells Which Calculate as Arrays
In issue #4557, the user complains, with some justification, about the way Excel handles certain calculations. We are not able to help with that problem. However, the user also notes a problem in Shared/Date when `isDateTime` has to evaluate a cell whose calculated value is an array. This is solved by flattening the calculated result to a single value. It became obvious while working on this change that the code to set `instanceArrayReturnType` was kind of awkward. Simpler methods `returnArrayAsArray` and `returnArrayAsValue` are added to `Spreadsheet`. Even these started out a bit awkward because `Spreadsheet::calculationEngine` was defined as nullable, which really isn't true. It is allocated by the constructor, and never freed except in the destructor. It is no longer nullable.
1 parent 7d1562a commit f1ce95e

File tree

8 files changed

+102
-43
lines changed

8 files changed

+102
-43
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,7 @@ public function __construct(?Spreadsheet $spreadsheet = null)
207207
public static function getInstance(?Spreadsheet $spreadsheet = null): self
208208
{
209209
if ($spreadsheet !== null) {
210-
$instance = $spreadsheet->getCalculationEngine();
211-
if (isset($instance)) {
212-
return $instance;
213-
}
210+
return $spreadsheet->getCalculationEngine();
214211
}
215212

216213
if (!self::$instance) {
@@ -220,6 +217,20 @@ public static function getInstance(?Spreadsheet $spreadsheet = null): self
220217
return self::$instance;
221218
}
222219

220+
/**
221+
* Intended for use only via a destructor.
222+
*
223+
* @internal
224+
*/
225+
public static function getInstanceOrNull(?Spreadsheet $spreadsheet = null): ?self
226+
{
227+
if ($spreadsheet !== null) {
228+
return $spreadsheet->getCalculationEngineOrNull();
229+
}
230+
231+
return null;
232+
}
233+
223234
/**
224235
* Flush the calculation cache for any existing instance of this class
225236
* but only if a Calculation instance exists.

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Reader;
44

5-
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
65
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
76
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
87
use PhpOffice\PhpSpreadsheet\Cell\DataType;
@@ -452,10 +451,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
452451
switch ($rel['Type']) {
453452
case "$xmlNamespaceBase/sheetMetadata":
454453
if ($this->fileExistsInArchive($zip, "xl/{$relTarget}")) {
455-
$excel->getCalculationEngine()
456-
?->setInstanceArrayReturnType(
457-
Calculation::RETURN_ARRAY_AS_ARRAY
458-
);
454+
$excel->returnArrayAsArray();
459455
}
460456

461457
break;

src/PhpSpreadsheet/Shared/Date.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,12 @@ public static function isDateTime(Cell $cell, mixed $value = null, bool $dateWit
374374
$selected = $worksheet->getSelectedCells();
375375

376376
try {
377-
$result = is_numeric($value ?? $cell->getCalculatedValue())
377+
if ($value === null) {
378+
$value = Functions::flattenSingleValue(
379+
$cell->getCalculatedValue()
380+
);
381+
}
382+
$result = is_numeric($value)
378383
&& self::isDateTimeFormat(
379384
$worksheet->getStyle(
380385
$cell->getCoordinate()

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class Spreadsheet implements JsonSerializable
5858
/**
5959
* Calculation Engine.
6060
*/
61-
private ?Calculation $calculationEngine;
61+
private Calculation $calculationEngine;
6262

6363
/**
6464
* Active sheet index.
@@ -477,7 +477,7 @@ public function __construct()
477477
public function __destruct()
478478
{
479479
$this->disconnectWorksheets();
480-
$this->calculationEngine = null;
480+
unset($this->calculationEngine);
481481
$this->cellXfCollection = [];
482482
$this->cellStyleXfCollection = [];
483483
$this->definedNames = [];
@@ -499,11 +499,25 @@ public function disconnectWorksheets(): void
499499
/**
500500
* Return the calculation engine for this worksheet.
501501
*/
502-
public function getCalculationEngine(): ?Calculation
502+
public function getCalculationEngine(): Calculation
503503
{
504504
return $this->calculationEngine;
505505
}
506506

507+
/**
508+
* Intended for use only via a destructor.
509+
*
510+
* @internal
511+
*/
512+
public function getCalculationEngineOrNull(): ?Calculation
513+
{
514+
if (!isset($this->calculationEngine)) { //* @phpstan-ignore-line
515+
return null;
516+
}
517+
518+
return $this->calculationEngine;
519+
}
520+
507521
/**
508522
* Get properties.
509523
*/
@@ -1120,21 +1134,19 @@ public function __clone()
11201134

11211135
$oldCalc = $this->calculationEngine;
11221136
$this->calculationEngine = new Calculation($this);
1123-
if ($oldCalc !== null) {
1124-
$this->calculationEngine
1125-
->setSuppressFormulaErrors(
1126-
$oldCalc->getSuppressFormulaErrors()
1127-
)
1128-
->setCalculationCacheEnabled(
1129-
$oldCalc->getCalculationCacheEnabled()
1130-
)
1131-
->setBranchPruningEnabled(
1132-
$oldCalc->getBranchPruningEnabled()
1133-
)
1134-
->setInstanceArrayReturnType(
1135-
$oldCalc->getInstanceArrayReturnType()
1136-
);
1137-
}
1137+
$this->calculationEngine
1138+
->setSuppressFormulaErrors(
1139+
$oldCalc->getSuppressFormulaErrors()
1140+
)
1141+
->setCalculationCacheEnabled(
1142+
$oldCalc->getCalculationCacheEnabled()
1143+
)
1144+
->setBranchPruningEnabled(
1145+
$oldCalc->getBranchPruningEnabled()
1146+
)
1147+
->setInstanceArrayReturnType(
1148+
$oldCalc->getInstanceArrayReturnType()
1149+
);
11381150
$usedKeys['calculationEngine'] = true;
11391151

11401152
$currentCollection = $this->cellStyleXfCollection;
@@ -1801,4 +1813,18 @@ public function replaceBuiltinNumberFormat(int $builtinFormatIndex, string $form
18011813
}
18021814
}
18031815
}
1816+
1817+
public function returnArrayAsArray(): void
1818+
{
1819+
$this->calculationEngine->setInstanceArrayReturnType(
1820+
Calculation::RETURN_ARRAY_AS_ARRAY
1821+
);
1822+
}
1823+
1824+
public function returnArrayAsValue(): void
1825+
{
1826+
$this->calculationEngine->setInstanceArrayReturnType(
1827+
Calculation::RETURN_ARRAY_AS_VALUE
1828+
);
1829+
}
18041830
}

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,8 @@ public function disconnectCells(): void
373373
*/
374374
public function __destruct()
375375
{
376-
Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
376+
Calculation::getInstanceOrNull($this->parent)
377+
?->clearCalculationCacheForWorksheet($this->title);
377378

378379
$this->disconnectCells();
379380
unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->chartCollection, $this->autoFilter);
@@ -909,7 +910,7 @@ public function setTitle(string $title, bool $updateFormulaCellReferences = true
909910
// Set title
910911
$this->title = $title;
911912

912-
if ($this->parent && $this->parent->getIndex($this, true) >= 0 && $this->parent->getCalculationEngine()) {
913+
if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
913914
// New title
914915
$newTitle = $this->getTitle();
915916
$this->parent->getCalculationEngine()

tests/PhpSpreadsheetTests/Calculation/StructuredReferenceFormulaTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function testStructuredReferenceHiddenHeaders(): void
3939
$result = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue();
4040
self::assertSame('Region', $result);
4141

42-
$spreadsheet->getCalculationEngine()?->flushInstance();
42+
$spreadsheet->getCalculationEngine()->flushInstance();
4343
$table->setShowHeaderRow(false);
4444

4545
$result = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue();

tests/PhpSpreadsheetTests/Shared/DateTest.php

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpOffice\PhpSpreadsheet\Shared\Date;
1212
use PhpOffice\PhpSpreadsheet\Spreadsheet;
1313
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
14+
use PHPUnit\Framework\Attributes\DataProvider;
1415
use PHPUnit\Framework\TestCase;
1516

1617
class DateTest extends TestCase
@@ -51,7 +52,7 @@ public function testSetExcelCalendarWithInvalidValue(): void
5152
self::assertFalse($result);
5253
}
5354

54-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeExcelToTimestamp1900')]
55+
#[DataProvider('providerDateTimeExcelToTimestamp1900')]
5556
public function testDateTimeExcelToTimestamp1900(float|int $expectedResult, float|int $excelDateTimeValue): void
5657
{
5758
if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) {
@@ -68,7 +69,7 @@ public static function providerDateTimeExcelToTimestamp1900(): array
6869
return require 'tests/data/Shared/Date/ExcelToTimestamp1900.php';
6970
}
7071

71-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeTimestampToExcel1900')]
72+
#[DataProvider('providerDateTimeTimestampToExcel1900')]
7273
public function testDateTimeTimestampToExcel1900(float|int $expectedResult, float|int|string $unixTimestamp): void
7374
{
7475
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
@@ -82,7 +83,7 @@ public static function providerDateTimeTimestampToExcel1900(): array
8283
return require 'tests/data/Shared/Date/TimestampToExcel1900.php';
8384
}
8485

85-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeDateTimeToExcel')]
86+
#[DataProvider('providerDateTimeDateTimeToExcel')]
8687
public function testDateTimeDateTimeToExcel(float|int $expectedResult, DateTimeInterface $dateTimeObject): void
8788
{
8889
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
@@ -99,7 +100,7 @@ public static function providerDateTimeDateTimeToExcel(): array
99100
/**
100101
* @param array{0: int, 1: int, 2: int, 3: int, 4: int, 5: float|int} $args Array containing year/month/day/hours/minutes/seconds
101102
*/
102-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeFormattedPHPToExcel1900')]
103+
#[DataProvider('providerDateTimeFormattedPHPToExcel1900')]
103104
public function testDateTimeFormattedPHPToExcel1900(mixed $expectedResult, ...$args): void
104105
{
105106
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
@@ -113,7 +114,7 @@ public static function providerDateTimeFormattedPHPToExcel1900(): array
113114
return require 'tests/data/Shared/Date/FormattedPHPToExcel1900.php';
114115
}
115116

116-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeExcelToTimestamp1904')]
117+
#[DataProvider('providerDateTimeExcelToTimestamp1904')]
117118
public function testDateTimeExcelToTimestamp1904(float|int $expectedResult, float|int $excelDateTimeValue): void
118119
{
119120
if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) {
@@ -130,7 +131,7 @@ public static function providerDateTimeExcelToTimestamp1904(): array
130131
return require 'tests/data/Shared/Date/ExcelToTimestamp1904.php';
131132
}
132133

133-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeTimestampToExcel1904')]
134+
#[DataProvider('providerDateTimeTimestampToExcel1904')]
134135
public function testDateTimeTimestampToExcel1904(mixed $expectedResult, float|int|string $unixTimestamp): void
135136
{
136137
Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
@@ -144,7 +145,7 @@ public static function providerDateTimeTimestampToExcel1904(): array
144145
return require 'tests/data/Shared/Date/TimestampToExcel1904.php';
145146
}
146147

147-
#[\PHPUnit\Framework\Attributes\DataProvider('providerIsDateTimeFormatCode')]
148+
#[DataProvider('providerIsDateTimeFormatCode')]
148149
public function testIsDateTimeFormatCode(mixed $expectedResult, string $format): void
149150
{
150151
$result = Date::isDateTimeFormatCode($format);
@@ -156,7 +157,7 @@ public static function providerIsDateTimeFormatCode(): array
156157
return require 'tests/data/Shared/Date/FormatCodes.php';
157158
}
158159

159-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeExcelToTimestamp1900Timezone')]
160+
#[DataProvider('providerDateTimeExcelToTimestamp1900Timezone')]
160161
public function testDateTimeExcelToTimestamp1900Timezone(float|int $expectedResult, float|int $excelDateTimeValue, string $timezone): void
161162
{
162163
if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) {
@@ -240,6 +241,27 @@ public function testVarious(): void
240241
$spreadsheet->disconnectWorksheets();
241242
}
242243

244+
public function testArray(): void
245+
{
246+
$spreadsheet = new Spreadsheet();
247+
$spreadsheet->returnArrayAsArray();
248+
$sheet = $spreadsheet->getActiveSheet();
249+
$sheet->setCellValue('A1', 45000);
250+
$sheet->setCellValue('A2', 44000);
251+
$sheet->setCellValue('A3', 46000);
252+
$sheet->setCellValue('C1', '=SORT(A1:A3)');
253+
$sheet->setCellValue('D1', '=SORT(A1:A3)');
254+
$sheet->getStyle('C1')
255+
->getNumberFormat()
256+
->setFormatCode('yyyy-mm-dd');
257+
self::assertTrue(Date::isDateTime($sheet->getCell('C1')));
258+
self::assertFalse(Date::isDateTime($sheet->getCell('D1')));
259+
self::assertIsArray(
260+
$sheet->getCell('C1')->getCalculatedValue()
261+
);
262+
$spreadsheet->disconnectWorksheets();
263+
}
264+
243265
public function testRoundMicroseconds(): void
244266
{
245267
$dti = new DateTime('2000-01-02 03:04:05.999999');

tests/PhpSpreadsheetTests/SpreadsheetCopyCloneTest.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ public function testCopyClone(string $type): void
4949
} else {
5050
$spreadsheet2 = $this->spreadsheet2 = clone $spreadsheet;
5151
}
52-
self::assertSame($spreadsheet, $spreadsheet->getCalculationEngine()?->getSpreadsheet());
53-
self::assertSame($spreadsheet2, $spreadsheet2->getCalculationEngine()?->getSpreadsheet());
52+
self::assertSame($spreadsheet, $spreadsheet->getCalculationEngine()->getSpreadsheet());
53+
self::assertSame($spreadsheet2, $spreadsheet2->getCalculationEngine()->getSpreadsheet());
5454
self::assertSame('A3', $sheet->getSelectedCells());
5555
$copysheet = $spreadsheet2->getActiveSheet();
5656
self::assertSame('A3', $copysheet->getSelectedCells());
@@ -132,7 +132,6 @@ public function testCopyClone2(string $type, bool $suppress, bool $cache, bool $
132132
{
133133
$spreadsheet = $this->spreadsheet = new Spreadsheet();
134134
$calc = $spreadsheet->getCalculationEngine();
135-
self::assertNotNull($calc);
136135
$calc->setSuppressFormulaErrors($suppress);
137136
$calc->setCalculationCacheEnabled($cache);
138137
$calc->setBranchPruningEnabled($pruning);
@@ -143,7 +142,6 @@ public function testCopyClone2(string $type, bool $suppress, bool $cache, bool $
143142
$spreadsheet2 = $this->spreadsheet2 = clone $spreadsheet;
144143
}
145144
$calc2 = $spreadsheet2->getCalculationEngine();
146-
self::assertNotNull($calc2);
147145
self::assertSame($suppress, $calc2->getSuppressFormulaErrors());
148146
self::assertSame($cache, $calc2->getCalculationCacheEnabled());
149147
self::assertSame($pruning, $calc2->getBranchPruningEnabled());

0 commit comments

Comments
 (0)