Skip to content

Commit c944b1a

Browse files
authored
Merge pull request #4697 from oleibman/issue4696
Unexpected Exception in Php DateTime
2 parents a61f9f1 + 938ba72 commit c944b1a

File tree

10 files changed

+199
-8
lines changed

10 files changed

+199
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
2929

3030
### Fixed
3131

32-
- Nothing yet.
32+
- Unexpected Exception in Php DateTime. [Issue #4696](https://github.com/PHPOffice/PhpSpreadsheet/issues/4696) [Issue #917](https://github.com/PHPOffice/PhpSpreadsheet/issues/917) [PR #4697](https://github.com/PHPOffice/PhpSpreadsheet/pull/4697)
3333

3434
## 2025-10-25 - 5.2.0
3535

src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
88
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
99
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
10+
use Throwable;
1011

1112
class Helpers
1213
{
@@ -56,6 +57,12 @@ public static function getDateValue(mixed $dateValue, bool $allowBool = true): f
5657
throw new Exception(ExcelError::NAN());
5758
}
5859

60+
try {
61+
SharedDateHelper::excelToDateTimeObject((float) $dateValue);
62+
} catch (Throwable) {
63+
throw new Exception(ExcelError::NAN());
64+
}
65+
5966
return (float) $dateValue;
6067
}
6168

src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
66
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
7+
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
78
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
9+
use Throwable;
810

911
class TimeParts
1012
{
@@ -44,6 +46,11 @@ public static function hour(mixed $timeValue): array|string|int
4446
}
4547

4648
// Execute function
49+
try {
50+
SharedDateHelper::excelToDateTimeObject($timeValue);
51+
} catch (Throwable) {
52+
return ExcelError::NAN();
53+
}
4754
$timeValue = fmod($timeValue, 1);
4855
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
4956
SharedDateHelper::roundMicroseconds($timeValue);
@@ -85,6 +92,11 @@ public static function minute(mixed $timeValue): array|string|int
8592
}
8693

8794
// Execute function
95+
try {
96+
SharedDateHelper::excelToDateTimeObject($timeValue);
97+
} catch (Throwable) {
98+
return ExcelError::NAN();
99+
}
88100
$timeValue = fmod($timeValue, 1);
89101
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
90102
SharedDateHelper::roundMicroseconds($timeValue);
@@ -126,6 +138,11 @@ public static function second(mixed $timeValue): array|string|int
126138
}
127139

128140
// Execute function
141+
try {
142+
SharedDateHelper::excelToDateTimeObject($timeValue);
143+
} catch (Throwable) {
144+
return ExcelError::NAN();
145+
}
129146
$timeValue = fmod($timeValue, 1);
130147
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
131148
SharedDateHelper::roundMicroseconds($timeValue);

src/PhpSpreadsheet/Shared/Date.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpOffice\PhpSpreadsheet\Exception;
1212
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
1313
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
14+
use Throwable;
1415

1516
class Date
1617
{
@@ -376,15 +377,18 @@ public static function isDateTime(Cell $cell, mixed $value = null, bool $dateWit
376377
$cell->getCalculatedValue()
377378
);
378379
}
379-
$result = is_numeric($value)
380-
&& self::isDateTimeFormat(
380+
if (is_numeric($value)) {
381+
$result = self::isDateTimeFormat(
381382
$worksheet->getStyle(
382383
$cell->getCoordinate()
383384
)->getNumberFormat(),
384385
$dateWithoutTimeOkay
385386
);
386-
} catch (Exception) {
387-
// Result is already false, so no need to actually do anything here
387+
/** @var float|int $value */
388+
self::excelToDateTimeObject($value);
389+
}
390+
} catch (Throwable) {
391+
$result = false;
388392
}
389393
$worksheet->setSelectedCells($selected);
390394
$spreadsheet->setActiveSheetIndex($index);

src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat;
44

55
use PhpOffice\PhpSpreadsheet\Shared\Date;
6+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
7+
use Throwable;
68

79
class DateFormatter
810
{
@@ -161,7 +163,11 @@ public static function format(mixed $value, string $format): string
161163
$callback = [self::class, 'escapeQuotesCallback'];
162164
$format = (string) preg_replace_callback('/"(.*)"/U', $callback, $format);
163165

164-
$dateObj = Date::excelToDateTimeObject($value);
166+
try {
167+
$dateObj = Date::excelToDateTimeObject($value);
168+
} catch (Throwable) {
169+
return StringHelper::convertToString($value);
170+
}
165171
// If the colon preceding minute had been quoted, as happens in
166172
// Excel 2003 XML formats, m will not have been changed to i above.
167173
// Change it now.

src/PhpSpreadsheet/Worksheet/AutoFilter.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PhpOffice\PhpSpreadsheet\Shared\Date;
1616
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
1717
use Stringable;
18+
use Throwable;
1819

1920
class AutoFilter implements Stringable
2021
{
@@ -339,7 +340,12 @@ protected static function filterTestInDateGroupSet(mixed $cellValue, array $data
339340
$timeZone = new DateTimeZone('UTC');
340341

341342
if (is_numeric($cellValue)) {
342-
$dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone);
343+
try {
344+
$dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone);
345+
} catch (Throwable) {
346+
return false;
347+
}
348+
343349
$cellValue = (float) $cellValue;
344350
if ($cellValue < 1) {
345351
// Just the time part
@@ -489,7 +495,12 @@ protected static function filterTestInPeriodDateSet(mixed $cellValue, array $mon
489495
}
490496

491497
if (is_numeric($cellValue)) {
492-
$dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC'));
498+
try {
499+
$dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC'));
500+
} catch (Throwable) {
501+
return false;
502+
}
503+
493504
$dateValue = (int) $dateObject->format('m');
494505
if (in_array($dateValue, $monthSet)) {
495506
return true;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime;
6+
7+
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\Helpers;
8+
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class HelpersTest extends TestCase
12+
{
13+
public function testGetDateValueBadObject(): void
14+
{
15+
$this->expectException(CalcExp::class);
16+
$this->expectExceptionMessage('#VALUE!');
17+
Helpers::getDateValue($this);
18+
}
19+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Shared;
6+
7+
use PhpOffice\PhpSpreadsheet\Cell\DataType;
8+
use PhpOffice\PhpSpreadsheet\Shared\Date;
9+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class Issue4696Test extends TestCase
14+
{
15+
private ?Spreadsheet $spreadsheet = null;
16+
17+
protected function tearDown(): void
18+
{
19+
if ($this->spreadsheet !== null) {
20+
$this->spreadsheet->disconnectWorksheets();
21+
$this->spreadsheet = null;
22+
}
23+
}
24+
25+
#[DataProvider('providerIsDateTime')]
26+
public function testIsDateTime(bool $expectedResult, string $expectedFormatted, int|float|string $value): void
27+
{
28+
$this->spreadsheet = new Spreadsheet();
29+
$sheet = $this->spreadsheet->getActiveSheet();
30+
if (is_string($value) && $value[0] !== '=') {
31+
$sheet->getCell('A1')->setValueExplicit($value, DataType::TYPE_STRING);
32+
} else {
33+
$sheet->getCell('A1')->setValue($value);
34+
}
35+
$sheet->getStyle('A1')->getNumberFormat()
36+
->setFormatCode('yyyy-mm-dd');
37+
self::assertSame(
38+
$expectedResult,
39+
Date::isDateTime($sheet->getCell('A1'))
40+
);
41+
self::assertSame(
42+
$expectedFormatted,
43+
$sheet->getCell('A1')->getFormattedValue()
44+
);
45+
}
46+
47+
public static function providerIsDateTime(): array
48+
{
49+
return [
50+
'valid integer' => [true, '1903-12-31', 1461],
51+
'valid integer stored as string' => [true, '1904-01-01', '1462'],
52+
'valid integer stored as concatenated string' => [true, '1904-01-01', '="14"&"62"'],
53+
'valid float' => [true, '1903-12-31', 1461.5],
54+
'valid float stored as string' => [true, '1903-12-31', '1461.5'],
55+
'out-of-range integer' => [false, '7000989091802000122', 7000989091802000122],
56+
'out-of-range float' => [false, '7.000989091802E+18', 7000989091802000122.1],
57+
'out-of-range float stored as string' => [false, '7000989091802000122.1', '7000989091802000122.1'],
58+
'non-numeric' => [false, 'xyz', 'xyz'],
59+
'issue 917' => [false, '5e8630b8-603c-43fe-b038-6154a3f893ab', '5e8630b8-603c-43fe-b038-6154a3f893ab'],
60+
];
61+
}
62+
63+
#[DataProvider('providerOtherFunctions')]
64+
public function testOtherFunctions(string $function): void
65+
{
66+
$this->spreadsheet = new Spreadsheet();
67+
$sheet = $this->spreadsheet->getActiveSheet();
68+
$sheet->getCell('A1')->setValue(7000989091802000122);
69+
$sheet->getCell('A3')->setValue(39107); // 2007-01-25
70+
$sheet->getCell('A4')->setValue(39767); // 2008-11-15
71+
$sheet->getCell('A5')->setValue(2);
72+
$sheet->getCell('A6')->setValue(1);
73+
$sheet->getCell('B1')->setValue($function);
74+
self::assertSame(
75+
'#NUM!',
76+
$sheet->getCell('B1')->getFormattedValue()
77+
);
78+
}
79+
80+
public static function providerOtherFunctions(): array
81+
{
82+
return [
83+
['=YEAR(A1)'],
84+
['=MONTH(A1)'],
85+
['=DAY(A1)'],
86+
['=DAYS(A1,A1)'],
87+
['=DAYS360(A1,A1)'],
88+
['=DATEDIF(A1,A1,"D")'],
89+
['=HOUR(A1)'],
90+
['=MINUTE(A1)'],
91+
['=SECOND(A1)'],
92+
['=WEEKNUM(A1)'],
93+
['=ISOWEEKNUM(A1)'],
94+
['=WEEKDAY(A1)'],
95+
['=COUPDAYBS(A1,A4,A5,A6)'],
96+
['=COUPDAYS(A3,A2,A5,A6)'],
97+
['=COUPDAYSNC(A3,A2,A5,A6)'],
98+
['=COUPNCD(A3,A2,A5,A6)'],
99+
['=COUPNUM(A3,A2,A5,A6)'],
100+
['=COUPPCD(A3,A2,A5,A6)'],
101+
];
102+
}
103+
}

tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public function testYears(array $expectedVisible, string $rule): void
4747
}
4848
++$row;
4949
$sheet->getCell("A$row")->setValue('=DATE(2041, 1, 1)'); // beyond epoch
50+
++$row;
51+
$sheet->getCell("A$row")->setValue(7000989091802000122); // issue 4696
5052
++$row; // empty row at end
5153
$this->maxRow = $maxRow = $row;
5254
$autoFilter = $sheet->getAutoFilter();

tests/PhpSpreadsheetTests/Worksheet/AutoFilter/DateGroupTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,28 @@ public function testYearMonthDayGroup(): void
5454
self::assertEquals([6], $this->getVisible());
5555
}
5656

57+
public function testIssue4696(): void
58+
{
59+
$year = 2011;
60+
$sheet = $this->initSheet($year);
61+
$sheet->getCell('A2')->setValue(7000989091802000122);
62+
$columnFilter = $sheet->getAutoFilter()->getColumn('C');
63+
$columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER);
64+
$columnFilter->createRule()
65+
->setRule(
66+
Rule::AUTOFILTER_COLUMN_RULE_EQUAL,
67+
[
68+
'year' => $year,
69+
'month' => 12,
70+
'day' => 6,
71+
]
72+
)
73+
->setRuleType(
74+
Rule::AUTOFILTER_RULETYPE_DATEGROUP
75+
);
76+
self::assertEquals([6], $this->getVisible());
77+
}
78+
5779
public function testYearMonthDayHourMinuteSecond1Group(): void
5880
{
5981
$year = 2011;

0 commit comments

Comments
 (0)