Skip to content

Commit ea97afb

Browse files
authored
Merge pull request #4562 from oleibman/issue4557date
Shared/Date::isDateTime Handle Cells Which Calculate as Arrays
2 parents a907f19 + c4cd1b3 commit ea97afb

File tree

10 files changed

+116
-63
lines changed

10 files changed

+116
-63
lines changed

CHANGELOG.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
3636

3737
### Fixed
3838

39-
- Xlsx Writer eliminate xml:space from non-text nodes. [Issue #4542](https://github.com/PHPOffice/PhpSpreadsheet/issues/4542) [PR #4556](https://github.com/PHPOffice/PhpSpreadsheet/pull/4556)
40-
41-
## 2025-07-23 - 4.5.0
39+
- Shared/Date::isDateTime handle cells which calculate as arrays. [Issue #4557](https://github.com/PHPOffice/PhpSpreadsheet/issues/4557) [PR #4562](https://github.com/PHPOffice/PhpSpreadsheet/pull/4562)
40+
- Xlsx Writer eliminate xml:space from non-text nodes. [Issue #4542](https://github.com/PHPOffice/PhpSpreadsheet/issues/4542) [PR #4556](https://github.com/PHPOffice/PhpSpreadsheet/pull/4556)## 2025-07-23 - 4.5.0
4241

4342
### Added
4443

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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,7 @@ public static function convertIsoDate(mixed $value): float|int
172172
throw new Exception("Invalid string $value supplied for datatype Date");
173173
}
174174

175-
$newValue = self::PHPToExcel($date);
176-
if ($newValue === false) {
177-
throw new Exception("Invalid string $value supplied for datatype Date");
178-
}
175+
$newValue = self::dateTimeToExcel($date);
179176

180177
if (preg_match('/^\s*\d?\d:\d\d(:\d\d([.]\d+)?)?\s*(am|pm)?\s*$/i', $value) == 1) {
181178
$newValue = fmod($newValue, 1.0);
@@ -374,7 +371,12 @@ public static function isDateTime(Cell $cell, mixed $value = null, bool $dateWit
374371
$selected = $worksheet->getSelectedCells();
375372

376373
try {
377-
$result = is_numeric($value ?? $cell->getCalculatedValue())
374+
if ($value === null) {
375+
$value = Functions::flattenSingleValue(
376+
$cell->getCalculatedValue()
377+
);
378+
}
379+
$result = is_numeric($value)
378380
&& self::isDateTimeFormat(
379381
$worksheet->getStyle(
380382
$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/Reader/Ods/DefinedNamesTest.php

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
66

7-
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
87
use PhpOffice\PhpSpreadsheet\Reader\Ods;
98
use PHPUnit\Framework\TestCase;
109

@@ -15,10 +14,7 @@ public function testDefinedNamesValue(): void
1514
$filename = 'tests/data/Reader/Ods/DefinedNames.ods';
1615
$reader = new Ods();
1716
$spreadsheet = $reader->load($filename);
18-
$calculation = Calculation::getInstance($spreadsheet);
19-
$calculation->setInstanceArrayReturnType(
20-
Calculation::RETURN_ARRAY_AS_VALUE
21-
);
17+
$spreadsheet->returnArrayAsValue();
2218
$worksheet = $spreadsheet->getActiveSheet();
2319

2420
$firstDefinedNameValue = $worksheet->getCell('First')->getValue();
@@ -36,10 +32,7 @@ public function testDefinedNamesApostropheValue(): void
3632
$filename = 'tests/data/Reader/Ods/DefinedNames.apostrophe.ods';
3733
$reader = new Ods();
3834
$spreadsheet = $reader->load($filename);
39-
$calculation = Calculation::getInstance($spreadsheet);
40-
$calculation->setInstanceArrayReturnType(
41-
Calculation::RETURN_ARRAY_AS_VALUE
42-
);
35+
$spreadsheet->returnArrayAsValue();
4336
$worksheet = $spreadsheet->getActiveSheet();
4437
self::assertSame("apo'strophe", $worksheet->getTitle());
4538

@@ -58,10 +51,7 @@ public function testDefinedNamesArray(): void
5851
$filename = 'tests/data/Reader/Ods/DefinedNames.ods';
5952
$reader = new Ods();
6053
$spreadsheet = $reader->load($filename);
61-
$calculation = Calculation::getInstance($spreadsheet);
62-
$calculation->setInstanceArrayReturnType(
63-
Calculation::RETURN_ARRAY_AS_ARRAY
64-
);
54+
$spreadsheet->returnArrayAsArray();
6555
$worksheet = $spreadsheet->getActiveSheet();
6656

6757
$firstDefinedNameValue = $worksheet->getCell('First')->getValue();

tests/PhpSpreadsheetTests/Shared/DateTest.php

Lines changed: 38 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
@@ -38,10 +39,15 @@ public function testSetExcelCalendar(): void
3839
Date::CALENDAR_WINDOWS_1900,
3940
];
4041

42+
$spreadsheet = new Spreadsheet();
4143
foreach ($calendarValues as $calendarValue) {
4244
$result = Date::setExcelCalendar($calendarValue);
4345
self::assertTrue($result);
46+
$result = $spreadsheet->setExcelCalendar($calendarValue);
47+
self::assertTrue($result);
4448
}
49+
self::assertFalse($spreadsheet->setExcelCalendar(0));
50+
$spreadsheet->disconnectWorksheets();
4551
}
4652

4753
public function testSetExcelCalendarWithInvalidValue(): void
@@ -51,7 +57,7 @@ public function testSetExcelCalendarWithInvalidValue(): void
5157
self::assertFalse($result);
5258
}
5359

54-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeExcelToTimestamp1900')]
60+
#[DataProvider('providerDateTimeExcelToTimestamp1900')]
5561
public function testDateTimeExcelToTimestamp1900(float|int $expectedResult, float|int $excelDateTimeValue): void
5662
{
5763
if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) {
@@ -68,7 +74,7 @@ public static function providerDateTimeExcelToTimestamp1900(): array
6874
return require 'tests/data/Shared/Date/ExcelToTimestamp1900.php';
6975
}
7076

71-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeTimestampToExcel1900')]
77+
#[DataProvider('providerDateTimeTimestampToExcel1900')]
7278
public function testDateTimeTimestampToExcel1900(float|int $expectedResult, float|int|string $unixTimestamp): void
7379
{
7480
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
@@ -82,7 +88,7 @@ public static function providerDateTimeTimestampToExcel1900(): array
8288
return require 'tests/data/Shared/Date/TimestampToExcel1900.php';
8389
}
8490

85-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeDateTimeToExcel')]
91+
#[DataProvider('providerDateTimeDateTimeToExcel')]
8692
public function testDateTimeDateTimeToExcel(float|int $expectedResult, DateTimeInterface $dateTimeObject): void
8793
{
8894
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
@@ -99,7 +105,7 @@ public static function providerDateTimeDateTimeToExcel(): array
99105
/**
100106
* @param array{0: int, 1: int, 2: int, 3: int, 4: int, 5: float|int} $args Array containing year/month/day/hours/minutes/seconds
101107
*/
102-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeFormattedPHPToExcel1900')]
108+
#[DataProvider('providerDateTimeFormattedPHPToExcel1900')]
103109
public function testDateTimeFormattedPHPToExcel1900(mixed $expectedResult, ...$args): void
104110
{
105111
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
@@ -113,7 +119,7 @@ public static function providerDateTimeFormattedPHPToExcel1900(): array
113119
return require 'tests/data/Shared/Date/FormattedPHPToExcel1900.php';
114120
}
115121

116-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeExcelToTimestamp1904')]
122+
#[DataProvider('providerDateTimeExcelToTimestamp1904')]
117123
public function testDateTimeExcelToTimestamp1904(float|int $expectedResult, float|int $excelDateTimeValue): void
118124
{
119125
if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) {
@@ -130,7 +136,7 @@ public static function providerDateTimeExcelToTimestamp1904(): array
130136
return require 'tests/data/Shared/Date/ExcelToTimestamp1904.php';
131137
}
132138

133-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeTimestampToExcel1904')]
139+
#[DataProvider('providerDateTimeTimestampToExcel1904')]
134140
public function testDateTimeTimestampToExcel1904(mixed $expectedResult, float|int|string $unixTimestamp): void
135141
{
136142
Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
@@ -144,7 +150,7 @@ public static function providerDateTimeTimestampToExcel1904(): array
144150
return require 'tests/data/Shared/Date/TimestampToExcel1904.php';
145151
}
146152

147-
#[\PHPUnit\Framework\Attributes\DataProvider('providerIsDateTimeFormatCode')]
153+
#[DataProvider('providerIsDateTimeFormatCode')]
148154
public function testIsDateTimeFormatCode(mixed $expectedResult, string $format): void
149155
{
150156
$result = Date::isDateTimeFormatCode($format);
@@ -156,7 +162,7 @@ public static function providerIsDateTimeFormatCode(): array
156162
return require 'tests/data/Shared/Date/FormatCodes.php';
157163
}
158164

159-
#[\PHPUnit\Framework\Attributes\DataProvider('providerDateTimeExcelToTimestamp1900Timezone')]
165+
#[DataProvider('providerDateTimeExcelToTimestamp1900Timezone')]
160166
public function testDateTimeExcelToTimestamp1900Timezone(float|int $expectedResult, float|int $excelDateTimeValue, string $timezone): void
161167
{
162168
if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) {
@@ -198,6 +204,9 @@ public function testVarious(): void
198204

199205
$date = Date::PHPToExcel('2020-01-01');
200206
self::assertEquals(43831.0, $date);
207+
$phpDate = new DateTime('2020-01-02T00:00Z');
208+
$date = Date::PHPToExcel($phpDate);
209+
self::assertEquals(43832.0, $date);
201210

202211
$spreadsheet = new Spreadsheet();
203212
$sheet = $spreadsheet->getActiveSheet();
@@ -240,6 +249,27 @@ public function testVarious(): void
240249
$spreadsheet->disconnectWorksheets();
241250
}
242251

252+
public function testArray(): void
253+
{
254+
$spreadsheet = new Spreadsheet();
255+
$spreadsheet->returnArrayAsArray();
256+
$sheet = $spreadsheet->getActiveSheet();
257+
$sheet->setCellValue('A1', 45000);
258+
$sheet->setCellValue('A2', 44000);
259+
$sheet->setCellValue('A3', 46000);
260+
$sheet->setCellValue('C1', '=SORT(A1:A3)');
261+
$sheet->setCellValue('D1', '=SORT(A1:A3)');
262+
$sheet->getStyle('C1')
263+
->getNumberFormat()
264+
->setFormatCode('yyyy-mm-dd');
265+
self::assertTrue(Date::isDateTime($sheet->getCell('C1')));
266+
self::assertFalse(Date::isDateTime($sheet->getCell('D1')));
267+
self::assertIsArray(
268+
$sheet->getCell('C1')->getCalculatedValue()
269+
);
270+
$spreadsheet->disconnectWorksheets();
271+
}
272+
243273
public function testRoundMicroseconds(): void
244274
{
245275
$dti = new DateTime('2000-01-02 03:04:05.999999');

0 commit comments

Comments
 (0)